 /*******************************************************************************
  * Copyright (c) 2000, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.update.core;

 import java.io.BufferedOutputStream ;
 import java.io.File ;
 import java.io.FileNotFoundException ;
 import java.io.FileOutputStream ;
 import java.io.IOException ;
 import java.io.InputStream ;
 import java.io.OutputStream ;
 import java.net.URL ;
 import java.util.Date ;
 import java.util.HashMap ;
 import java.util.Iterator ;
 import java.util.Map ;
 import java.util.Properties ;
 import java.util.Set ;
 import java.util.StringTokenizer ;

 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.Path;
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.update.core.model.ContentEntryModel;
 import org.eclipse.update.core.model.InstallAbortedException;
 import org.eclipse.update.core.model.NonPluginEntryModel;
 import org.eclipse.update.core.model.PluginEntryModel;
 import org.eclipse.update.internal.core.FatalIOException;
 import org.eclipse.update.internal.core.FeatureDownloadException;
 import org.eclipse.update.internal.core.FileFragment;
 import org.eclipse.update.internal.core.InternalSiteManager;
 import org.eclipse.update.internal.core.LockManager;
 import org.eclipse.update.internal.core.Messages;
 import org.eclipse.update.internal.core.UpdateCore;
 import org.eclipse.update.internal.core.UpdateManagerUtils;

 /**
  * Base implementation of a feature content provider. This class provides a set
  * of helper methods useful for implementing feature content providers. In
  * particular, methods dealing with downloading and caching of feature files.
  * <p>
  * This class must be subclassed by clients.
  * </p>
  * <p>
  * <b>Note:</b> This class/interface is part of an interim API that is still under development and expected to
  * change significantly before reaching stability. It is being made available at this early stage to solicit feedback
  * from pioneering adopters on the understanding that any code that uses this API will almost certainly be broken
  * (repeatedly) as the API evolves.
  * </p>
  * @see org.eclipse.update.core.IFeatureContentProvider
  * @since 2.0
  */
 public abstract class FeatureContentProvider
     implements IFeatureContentProvider {

     private static final boolean SWITCH_COPY_LOCAL = true;
     //public static long timer = 0;

     //public static long first = 0;

     /**
      *
      */
     public class FileFilter {

         private IPath filterPath = null;

         /**
          * Constructor for FileFilter.
          */
         public FileFilter(String filter) {
             super();
             this.filterPath = new Path(filter);
         }

         /**
          * returns true if the name matches the rule
          */
         public boolean accept(String name) {

             if (name == null)
                 return false;

             // no '*' pattern matching
 // must be equals
 IPath namePath = new Path(name);
             if (filterPath.lastSegment().indexOf('*') == -1) {
                 return filterPath.equals(namePath);
             }

             // check same file extension if extension exists (a.txt/*.txt)
 // or same file name (a.txt,a.*)
 String extension = filterPath.getFileExtension();
             if (extension != null && !extension.equals("*")) { //$NON-NLS-1$
 if (!extension.equalsIgnoreCase(namePath.getFileExtension()))
                     return false;
             } else {
                 IPath noExtension = filterPath.removeFileExtension();
                 String fileName = noExtension.lastSegment();
                 if (!fileName.equals("*")) { //$NON-NLS-1$
 if (!namePath.lastSegment().startsWith(fileName))
                         return false;
                 }
             }

             // check same path
 IPath p1 = namePath.removeLastSegments(1);
             IPath p2 = filterPath.removeLastSegments(1);
             return p1.equals(p2);
         }

     }

     private URL base;
     private IFeature feature;
     private File tmpDir; // local work area for each provider
 public static final String JAR_EXTENSION = ".jar"; //$NON-NLS-1$

     private static final String DOT_PERMISSIONS = "permissions.properties"; //$NON-NLS-1$
 private static final String EXECUTABLES = "permissions.executable"; //$NON-NLS-1$

     /**
      * Feature content provider constructor
      *
      * @param base
      * feature URL. The interpretation of this URL is specific to
      * each content provider.
      * @since 2.0
      */
     public FeatureContentProvider(URL base) {
         this.base = base;
         this.feature = null;
     }

     /**
      * Returns the feature url.
      *
      * @see IFeatureContentProvider#getURL()
      */
     public URL getURL() {
         return base;
     }

     /**
      * Returns the feature associated with this content provider.
      *
      * @see IFeatureContentProvider#getFeature()
      */
     public IFeature getFeature() {
         return feature;
     }

     /**
      * Sets the feature associated with this content provider.
      *
      * @see IFeatureContentProvider#setFeature(IFeature)
      */
     public void setFeature(IFeature feature) {
         this.feature = feature;
     }

     /**
      * Returns the specified reference as a local file system reference. If
      * required, the file represented by the specified content reference is
      * first downloaded to the local system
      *
      * @param ref
      * content reference
      * @param monitor
      * progress monitor, can be <code>null</code>
      * @exception IOException
      * @exception CoreException
      * @since 2.0
      */
     public ContentReference asLocalReference(ContentReference ref, InstallMonitor monitor) throws IOException , CoreException {

         // check to see if this is already a local reference
 if (ref.isLocalReference())
             return ref;

         // check to see if we already have a local file for this reference
 String key = ref.toString();

         // need to synch as another thread my have created the file but
 // is still copying into it
 File localFile = null;
         FileFragment localFileFragment = null;
         Object keyLock = LockManager.getLock(key);
         synchronized (keyLock) {
             localFile = Utilities.lookupLocalFile(key);
             if (localFile != null) {
                 // check if the cached file is still valid (no newer version on
 // server)
 try {
                 if (UpdateManagerUtils.isSameTimestamp(ref.asURL(), localFile.lastModified()))
                     return ref.createContentReference(ref.getIdentifier(), localFile);
                 } catch(FatalIOException e) {
                     throw e;
                 } catch(IOException e) {
                     throw new FeatureDownloadException(NLS.bind(Messages.FeatureContentProvider_ExceptionDownloading, (new Object [] {getURL().toExternalForm()})), e);
                 }
             }

             if (localFile == null) {
                 localFileFragment = UpdateManagerUtils.lookupLocalFileFragment(key);
             }
             //
 // download the referenced file into local temporary area
 InputStream is = null;
             OutputStream os = null;
             long bytesCopied = 0;
             long inputLength = 0;
             boolean success = false;
             if (monitor != null) {
                 monitor.saveState();
                 monitor.setTaskName(Messages.FeatureContentProvider_Downloading);
                 monitor.subTask(ref.getIdentifier() + " "); //$NON-NLS-1$
 try {
                     monitor.setTotalCount(ref.getInputSize());
                 } catch (FatalIOException e) {
                     throw e;
                 } catch (IOException e) {
                     throw new FeatureDownloadException(NLS.bind(Messages.FeatureContentProvider_ExceptionDownloading, (new Object [] {getURL().toExternalForm()})), e);
                 }
                 monitor.showCopyDetails(true);
             }

             try {
                 //long startTime = System.nanoTime();
 if (localFileFragment != null && "http".equals(ref.asURL().getProtocol())) { //$NON-NLS-1$
 localFile = localFileFragment.getFile();
                     try {
                         // get partial input stream
 is = ref.getPartialInputStream(localFileFragment.getSize());
                         inputLength = ref.getInputSize() - localFileFragment.getSize();
                         // get output stream to append to file fragment
 os = new BufferedOutputStream (
                         // PAL foundation
 //new FileOutputStream(localFile, true));
 new FileOutputStream (localFile.getPath(), true));
                     } catch (FatalIOException e) {
                         throw e;
                     } catch (IOException e) {
                         try {
                             if (is != null)
                                 is.close();
                         } catch (IOException ioe) {
                         }
                         is = null;
                         os = null;
                         localFileFragment = null;
                         throw new FeatureDownloadException(NLS.bind(Messages.FeatureContentProvider_ExceptionDownloading, (new Object [] {getURL().toExternalForm()})), e);
                     }
                 }
                 if (is == null) {
                     // must download from scratch
 localFile = Utilities.createLocalFile(getWorkingDirectory(), null);
                     try {
                         is = ref.getInputStream();
                         inputLength = ref.getInputSize();
                     } catch (FatalIOException e) {
                         throw Utilities.newCoreException(NLS.bind(Messages.FeatureContentProvider_UnableToRetrieve, (new Object [] {ref})), e);
                     } catch (IOException e) {
                         throw new FeatureDownloadException(NLS.bind(Messages.FeatureContentProvider_ExceptionDownloading, (new Object [] {getURL().toExternalForm()})), e);
                     }

                     try {
                         os = new BufferedOutputStream (new FileOutputStream (localFile));
                     } catch (FileNotFoundException e) {
                         throw Utilities.newCoreException(NLS.bind(Messages.FeatureContentProvider_UnableToCreate, (new Object [] {localFile})), e);
                     }
                 }

                 Date start = new Date ();
                 if (localFileFragment != null) {
                     bytesCopied = localFileFragment.getSize();
                     if (monitor != null) {
                         monitor.setCopyCount(bytesCopied);
                     }
                 }

                 // Transfer as many bytes as possible from input to output stream
 long offset = UpdateManagerUtils.copy(is, os, monitor, inputLength);
                 if (offset != -1) {
                     bytesCopied += offset;
                     if (bytesCopied > 0) {
                         // preserve partially downloaded file
 UpdateManagerUtils.mapLocalFileFragment(key, new FileFragment(localFile, bytesCopied));
                     }
                     if (monitor.isCanceled()) {
                         String msg = Messages.Feature_InstallationCancelled;
                         throw new InstallAbortedException(msg, null);
                     } else {
                         throw new FeatureDownloadException(NLS.bind(Messages.FeatureContentProvider_ExceptionDownloading, (new Object [] {getURL().toExternalForm()})), new IOException ());
                     }
                 } else {
                     UpdateManagerUtils.unMapLocalFileFragment(key);
                 }

                 Date stop = new Date ();
                 long timeInseconds = (stop.getTime() - start.getTime()) / 1000;
                 // time in milliseconds /1000 = time in seconds
 InternalSiteManager.downloaded(
                     ref.getInputSize(),
                     (timeInseconds),
                     ref.asURL());

                 success = true;
                 //long endTime = System.nanoTime();
 // file is downloaded succesfully, map it
 Utilities.mapLocalFile(key, localFile);
                 
                 /*if (ref.asURL().toExternalForm().endsWith("jar")) {
                     synchronized(this.getClass()) {
                         timer += (endTime - startTime);
                         if (first == 0) {
                             first = endTime - startTime;
                         }
                     }
                 }*/
             } catch (ClassCastException e) {
                 throw Utilities.newCoreException(
                     NLS.bind(Messages.FeatureContentProvider_UnableToCreate, (new Object [] { localFile })),
                     e);
             } finally {
                 //Do not close IS if user cancel,
 //closing IS will read the entire Stream until the end
 if (success && is != null)
                     try {
                         is.close();
                     } catch (IOException e) {
                     }
                 if (os != null)
                     try {
                         os.close(); // should flush buffer stream
 } catch (IOException e) {
                     }

                 if (success || bytesCopied > 0) {
                     // set the timestamp on the temp file to match the remote
 // timestamp
 localFile.setLastModified(ref.getLastModified());
                 }
                 if (monitor != null)
                     monitor.restoreState();
             }
             LockManager.returnLock(key);
         } // end lock
 ContentReference reference =
             ref.createContentReference(ref.getIdentifier(), localFile);
         
         UpdateCore.getPlugin().getUpdateSession().markVisited(ref.asURL());
         
         return reference;
     }

     /**
      * Returns the specified reference as a local file. If required, the file
      * represented by the specified content reference is first downloaded to
      * the local system
      *
      * @param ref
      * content reference
      * @param monitor
      * progress monitor, can be <code>null</code>
      * @exception IOException
      * @exception CoreException
      * @since 2.0
      */
     public File asLocalFile(ContentReference ref, InstallMonitor monitor) throws IOException , CoreException {
         File file = ref.asFile();
         if (file != null && !SWITCH_COPY_LOCAL)
             return file;
         ContentReference localRef = asLocalReference(ref, monitor);
         file = localRef.asFile();
         return file;
     }

     /**
      * Returns working directory for this content provider
      *
      * @return working directory
      * @exception IOException
      * @since 2.0
      */
     protected File getWorkingDirectory() throws IOException {
         if (tmpDir == null)
             tmpDir = Utilities.createWorkingDirectory();
         return tmpDir;
     }

     /**
      * Returns the total size of all archives required for the specified
      * plug-in and non-plug-in entries (the "packaging" view).
      *
      * @see IFeatureContentProvider#getDownloadSizeFor(IPluginEntry[],
      * INonPluginEntry[])
      */
     public long getDownloadSizeFor(IPluginEntry[] pluginEntries, INonPluginEntry[] nonPluginEntries) {
         long result = 0;

         // if both are null or empty, return UNKNOWN size
 if ((pluginEntries == null || pluginEntries.length == 0) && (nonPluginEntries == null || nonPluginEntries.length == 0)) {
             return ContentEntryModel.UNKNOWN_SIZE;
         }

         // loop on plugin entries
 long size = 0;
         if (pluginEntries != null)
             for (int i = 0; i < pluginEntries.length; i++) {
                 size = ((PluginEntryModel) pluginEntries[i]).getDownloadSize();
                 if (size == ContentEntryModel.UNKNOWN_SIZE) {
                     return ContentEntryModel.UNKNOWN_SIZE;
                 }
                 result += size;
             }

         // loop on non plugin entries
 if (nonPluginEntries != null)
             for (int i = 0; i < nonPluginEntries.length; i++) {
                 size = ((NonPluginEntryModel) nonPluginEntries[i]).getDownloadSize();
                 if (size == ContentEntryModel.UNKNOWN_SIZE) {
                     return ContentEntryModel.UNKNOWN_SIZE;
                 }
                 result += size;
             }

         return result;
     }

     /**
      * Returns the total size of all files required for the specified plug-in
      * and non-plug-in entries (the "logical" view).
      *
      * @see IFeatureContentProvider#getInstallSizeFor(IPluginEntry[],
      * INonPluginEntry[])
      */
     public long getInstallSizeFor(IPluginEntry[] pluginEntries, INonPluginEntry[] nonPluginEntries) {
         long result = 0;

         // if both are null or empty, return UNKNOWN size
 if ((pluginEntries == null || pluginEntries.length == 0) && (nonPluginEntries == null || nonPluginEntries.length == 0)) {
             return ContentEntryModel.UNKNOWN_SIZE;
         }

         // loop on plugin entries
 long size = 0;
         if (pluginEntries != null)
             for (int i = 0; i < pluginEntries.length; i++) {
                 size = ((PluginEntryModel) pluginEntries[i]).getInstallSize();
                 if (size == ContentEntryModel.UNKNOWN_SIZE) {
                     return ContentEntryModel.UNKNOWN_SIZE;
                 }
                 result += size;
             }

         // loop on non plugin entries
 if (nonPluginEntries != null)
             for (int i = 0; i < nonPluginEntries.length; i++) {
                 size = ((NonPluginEntryModel) nonPluginEntries[i]).getInstallSize();
                 if (size == ContentEntryModel.UNKNOWN_SIZE) {
                     return ContentEntryModel.UNKNOWN_SIZE;
                 }
                 result += size;
             }

         return result;
     }

     /**
      * Returns the path identifier for a plugin entry. <code>plugins/&lt;pluginId>_&lt;pluginVersion>.jar</code>
      *
      * @return the path identifier
      */
     protected String getPathID(IPluginEntry entry) {
         return Site.DEFAULT_PLUGIN_PATH + entry.getVersionedIdentifier().toString() + JAR_EXTENSION;
     }

     /**
      * Returns the path identifer for a non plugin entry. <code>features/&lt;featureId>_&lt;featureVersion>/&lt;dataId></code>
      *
      * @return the path identifier
      */
     protected String getPathID(INonPluginEntry entry) {
         String nonPluginBaseID = Site.DEFAULT_FEATURE_PATH + feature.getVersionedIdentifier().toString() + "/"; //$NON-NLS-1$
 return nonPluginBaseID + entry.getIdentifier();
     }

     /**
      * Sets the permission of all the ContentReferences Check for the
      * .permissions contentReference and use it to set the permissions of other
      * ContentReference
      */
     protected void validatePermissions(ContentReference[] references) {

         if (references == null || references.length == 0)
             return;

         Map permissions = getPermissions(references);
         if (permissions.isEmpty())
             return;

         for (int i = 0; i < references.length; i++) {
             ContentReference contentReference = references[i];
             String id = contentReference.getIdentifier();
             Object value = null;
             if ((value = matchesOneRule(id, permissions)) != null) {
                 Integer permission = (Integer ) value;
                 contentReference.setPermission(permission.intValue());
             }
         }
     }

     /**
      * Returns the value of the matching rule or <code>null</code> if none
      * found. A rule is matched if the id is equals to a key, or if the id is
      * resolved by a key. if the id is <code>/path/file.txt</code> it is
      * resolved by <code>/path/*</code> or <code>/path/*.txt</code>
      *
      * @param id
      * the identifier
      * @param permissions
      * list of rules
      * @return Object the value of the matcing rule or <code>null</code>
      */
     private Object matchesOneRule(String id, Map permissions) {

         Set keySet = permissions.keySet();
         Iterator iter = keySet.iterator();
         while (iter.hasNext()) {
             FileFilter rule = (FileFilter) iter.next();
             if (rule.accept(id)) {
                 return permissions.get(rule);
             }
         }

         return null;
     }

     /*
      * returns the permission MAP
      */
     private Map getPermissions(ContentReference[] references) {

         Map result = new HashMap ();
         // search for .permissions
 boolean notfound = true;
         ContentReference permissionReference = null;
         for (int i = 0; i < references.length && notfound; i++) {
             ContentReference contentReference = references[i];
             if (DOT_PERMISSIONS.equals(contentReference.getIdentifier())) {
                 notfound = false;
                 permissionReference = contentReference;
             }
         }
         if (notfound)
             return result;

         Properties prop = new Properties ();
         InputStream propertyStream = null;
         try {
             try {
                 propertyStream = permissionReference.getInputStream();
                 prop.load(propertyStream);
             } finally {
                 if (propertyStream != null)
                     propertyStream.close();
             }
         } catch (IOException e) {
             UpdateCore.warn("", e); //$NON-NLS-1$
 }

         String executables = prop.getProperty(EXECUTABLES);
         if (executables == null)
             return result;

         StringTokenizer tokenizer = new StringTokenizer (executables, ","); //$NON-NLS-1$
 Integer defaultExecutablePermission = new Integer (ContentReference.DEFAULT_EXECUTABLE_PERMISSION);
         while (tokenizer.hasMoreTokens()) {
             FileFilter filter = new FileFilter(tokenizer.nextToken());
             result.put(filter, defaultExecutablePermission);
         }

         return result;
     }
 }

